/* ************************************************************************ */
/*                                                                          */
/*  kyototycoon-neko														*/
/*  HaXe/neko Library													    */
/*  Copyright (c)2011 François Nicaise                                      */
/*                                                                          */
/*  This program is free software; you can redistribute it and/or modify    */
/*  it under the terms of the GNU General Public License as published by    */
/*  the Free Software Foundation; either version 2 of the License, or       */
/*  (at your option) any later version.                                     */
/*                                                                          */
/*  This program is distributed in the hope that it will be useful,         */
/*  but WITHOUT ANY WARRANTY; without even the implied warranty of          */
/*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           */
/*  GNU General Public License for more details.                            */
/*                                                                          */
/*  You should have received a copy of the GNU General Public License       */
/*  along with this program; if not, write to the Free Software             */
/*  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
/*                                                                          */
/* ************************************************************************ */
package kt;

#if neko 
typedef BulkRecord = {
	key : String,
	value : String,
	expirationTime : Int,	
	dbidx : Int				// index of the database ( in case you open several databases in the same server )  
}

class Cursor {
	private var __c : Void;
	var db : RemoteDB;

	public var info : {key : String, value : String };
	
	public function new(db) {
		this.db = db;
		info = null;
	}

	public function setPointer( __c ) {
		this.__c = __c;
	}

	public function getPointer() {
		return __c;
	}

    /**
     * Jump the cursor to the first record for forward scan.
     * @return true on success, or false on failure.
	 * Note:  Depending on the type of database this will either be in order of insertion 
	 * or order determined by a hashing function ( source : http://kyoto-client.org/#cursor )
	 */
	public function jump() : Bool {
		return KTRemoteDbLib.cursor_jump( __c );
	}

	/**
	 * Jump the cursor to the last record for backward scan.
	 * @return true on success, or false on failure.
	 * @note This method is dedicated to tree databases.  Some database types, especially hash
	 * databases, may provide a dummy implementation.
	 */
	public function jumpBack() : Bool {
		return KTRemoteDbLib.cursor_jump_back(__c);
	}

    /**
     * Jump to a specific record
     * @return true on success, or false on failure.
     */
	public function jumpTo( key : String ) : Bool {
		return KTRemoteDbLib.cursor_jump_tokey( __c, key );
	}

	/**
	 * Step the cursor to the next record.
	 * @return true on success, or false on failure.
	 */
	public function step() : Bool{
		return KTRemoteDbLib.cursor_step(__c);
	}

	/**
	 * Step the cursor to the previous record.
	 * @return true on success, or false on failure.
	 */
	public function stepBack()  : Bool {
		return KTRemoteDbLib.cursor_step_back(__c);
	}

	/**
	 * Get the key of the current record.
	 * @param step true to move the cursor to the next record, or false for no move.
	 */
	public function getKey( step : Bool = false ) : String {
		return KTRemoteDbLib.cursor_get_key(__c, step );
	}

	/**
	 * Get the value of the current record.
	 * @param step true to move the cursor to the next record, or false for no move.
	 */
	public function getValue( step : Bool = false ) : String {
		return KTRemoteDbLib.cursor_get_value(__c, step );
	}

	/**
	 * Set the value of the current record.
	 * @param v the new value
	 * @param expirationTime the expiration time from now in seconds.  If it is negative, the absolute value
	 * is treated as the epoch time.
	 * @param step true to move the cursor to the next record, or false for no move.
	 */
	public function setValue( v : String, expirationTime = 0, step : Bool = false ) : Bool {
		return KTRemoteDbLib.cursor_set_value(__c, v, expirationTime, step );
	}

	/**
	 * Get a pair of the key and the value of the current record.
	 * @param step true to move the cursor to the next record, or false for no move.
	 */
	public function get( step : Bool = false  ) : { key:String, value : String } {
		var a = KTRemoteDbLib.cursor_get(__c, step);
		if( a == null ) return null;
		var base = db.buildArray( a );
		if( base == null ) return null;
		return {key:base[0],value:base[1]};
	}

	/**
	 * Get a pair of the key and the value of the current record 
	 * and move to next record
	 */
	public function next() : Bool  {
		var a = KTRemoteDbLib.cursor_get(__c, true);
		if( a == null ) return false;
		var base = db.buildArray( a );
		if( base == null ) return null;
		info = {key:base[0],value:base[1]};
		return true;
	}

	/**
	 * Remove the current record.
	 * @return true on success, or false on failure.
	 * @note If no record corresponds to the key, false is returned.  The cursor is moved to the
	 * next record implicitly.
	 */
	public function remove() : Bool {
		return KTRemoteDbLib.cursor_remove( __c );
	}

}

class RemoteDB {
	private var __c : Void;
	var databases : Array<String>;

	public function new() {
	}

	/**
		Open a connection to KyotoTycoon server.
	**/
	public function open( host : String = "", port : Int = 1978, timeout : Float = -1.0 ) {
		timeout *= 1.0; // avoid segfault when passing an integer instead of a float
		__c = KTRemoteDbLib.open( host, port, timeout );
	}

	/**
		* Register a database on current server
	**/
	public function registerDb( dbName : String ) {
		if( dbName == null ) return;
		if( databases == null ) databases = new Array();
		databases.push( dbName );
	}

	/**
		Close the current connection
	**/
	public function close() : Bool {
		if( cursors != null && cursors.length > 0 ) {
			for( c in cursors ) {
				try {
					KTRemoteDbLib.delete_cursor( c.getPointer() );
				} catch( e : Dynamic ) {
					// exception raised by a 501 error from ktserver
					// when calling rpc_delete method from ~Cursor()
				}
			}
		}
		return KTRemoteDbLib.close( __c );
	}

	/**
		Remove all records from the current database
	**/
	public function clear() : Bool {		
		return KTRemoteDbLib.clear( __c );
	}

	/**
		Synchronize updated content.
		By default, write on the physical device.
		If hard is set to false : write to the logical file system
		More info there : http://stackoverflow.com/questions/4299896/what-is-physical-synchronization-of-kyoto-cabinet-database
	**/
	public function synchronize( hard : Bool = true ) : Bool {		
		return KTRemoteDbLib.synchronize( __c, hard );
	}	

	/**
		get the number of records
	**/
	public function count() : Int {		
		return KTRemoteDbLib.count( __c );
	}	

	/**
		Get the size of the database
	**/
	public function size() : Int {		
		return KTRemoteDbLib.size( __c );
	}	

	/**
		Set the value of a record
		Replace old value if the record already exists
	**/
	public function set( key : String, value : String, expirationTime : Int = 0 ) : Bool {		
		return KTRemoteDbLib.set( __c, key, value, expirationTime );
	}	

	/**
		Add a record
		Return false when record already exists
	**/
	public function add( key : String, value : String, expirationTime : Int = 0 ) : Bool {		
		return KTRemoteDbLib.add( __c, key, value, expirationTime );
	}	

	/**
		Replace the value for a given record
	**/
	public function replace( key : String, value : String, expirationTime : Int = 0 ) : Bool {		
		return KTRemoteDbLib.replace( __c, key, value, expirationTime );
	}	

	/**
		Append the value for a given record
	**/
	public function append( key : String, value : String, expirationTime : Int = 0 ) : Bool {		
		return KTRemoteDbLib.append( __c, key, value, expirationTime );
	}	

	/**
		Add a number to the numeric value of a record
		Important : you need to create the record with 'increment' first not 'set' or any other method to use this feature
	**/
	public function increment( key : String, num : Int = 0, orig : Float = 0.0, xt : Int = 0 ) : Int {
		var negInf = ( orig == Math.NEGATIVE_INFINITY );
		var posInf = ( orig == Math.POSITIVE_INFINITY );
		var vorig : Int = if( negInf || posInf ) 0 else Std.int(orig);
		var infos : Array<Dynamic> = [key,num,vorig,negInf,posInf,xt];
		return KTRemoteDbLib.increment( __c, infos );
	}	

	/** 
		Add a float to the float value of a record
		Important : you need to create the record with 'incrementDouble' first not 'set' or any other method to use this feature
	**/		
	public function incrementDouble( key : String, num : Float = 0.0, orig : Float = 0.0, xt : Int = 0 ) : Float {
		var negInf = ( orig == Math.NEGATIVE_INFINITY );
		var posInf = ( orig == Math.POSITIVE_INFINITY );
		var vorig : Float = if( negInf || posInf ) 0.0 else orig;
		vorig += (0.1 - 0.1); // enforce float
		num += (0.1 - 0.1); // enforce float
		var infos : Array<Dynamic> = [key,num,vorig,negInf,posInf,xt];
		return KTRemoteDbLib.increment_double( __c, infos );
	}	

	/**
		Perform compare & swap
		The value of the record will be set to the new value only if the old value matches
	**/
	public function cas( key : String, oldValue : String, newValue : String, expirationTime : Int = 0 ) : Bool {
		return KTRemoteDbLib.cas( __c, key, oldValue, newValue, expirationTime );
	}	

	/**
		Remove a record
	**/
	public function remove( key : String ) : Bool {
		return KTRemoteDbLib.remove( __c, key );
	}

	/**
		Retrieve the value of a record
	**/
	public function get( key : String, check = false ) : String {
		var r : String = KTRemoteDbLib.get( __c, key );
		try {
			var l = r.length;
		}catch(e : Dynamic ){
			return null;
		}
		return r;
	}

	/**
		Scan the database and eliminate regions of expired records.
		steps : the number of steps. If it is not more than 0, the whole region is scanned.
		Further information there : http://fallabs.com/kyototycoon/spex.html ( see Inside Expiration )  
	**/
	public function vacuum( steps : Int = 0 ) : Bool {
		return KTRemoteDbLib.vacuum( __c, steps );
	}

	/**
		Get keys matching a prefix string.
		max : the maximum number to retrieve. If it is negative, no limit is specified.
	**/
	public function matchPrefix( prefix : String, max : Int = -1 ) : Array<String> {
		return buildArray( KTRemoteDbLib.match_prefix( __c, prefix, max ) );
	}

	/**
		Get keys matching a regular expression string
		max : the maximum number to retrieve. If it is negative, no limit is specified.
	**/
	public function matchRegex( regex : String, max : Int = -1 ) : Array<String> {
		return buildArray( KTRemoteDbLib.match_regex( __c, regex, max ) );
	}

	/**
		Set the target database.
		For instance use db.setTarget("test.kch") to switch to the test.kch store on current host 
	**/
	public function setTarget( dbName : String ) {
		return KTRemoteDbLib.set_target( __c, dbName );
	}

	/**
		Retrieve info from the current socket ( port )
	**/
	public function expression() : String {
		return KTRemoteDbLib.expression(__c);
	}

	/**
		Get the miscellaneous status information
	**/
	public function status() : Hash<String> {
		return AAS2HS( buildArrayArray( KTRemoteDbLib.status(__c) ) );
	}

	/** 
		Get the current dbidbx
	**/
	public function getDbIdx( dbName : String = null ) : Int {
		// Note, there aren't any possibility to get relevant server information using the 
		// status method. status() does not return any index for the current target

		if( dbName == null ) return 0;

		for( i in 0...databases.length ) {
			var cur = databases[i];
			if( cur == dbName ) return i;
		}

		return 0;
	}

	/**
		Set multiple records at once
		atomic : true to perform all operations atomically, or false for non-atomic operations.
	**/
	public function setBulk( recs : Hash<String>, expirationTime : Int = 0, atomic : Bool = true) : Int {
		return KTRemoteDbLib.set_bulk( __c, HS2AAS( recs ), expirationTime, atomic );
	}

	/**
		Remove multiple records at once
		atomic : true to perform all operations atomically, or false for non-atomic operations.
	**/
	public function removeBulk( keys : Array<String>, atomic : Bool = true ) : Int {
		return KTRemoteDbLib.remove_bulk( __c, keys, atomic );
	}

	/**
		Get multiple records at once
		atomic : true to perform all operations atomically, or false for non-atomic operations.
	**/
	public function getBulk( keys : Array<String>, atomic : Bool = true ) : Hash<String> {
		return AAS2HS( buildArrayArray( KTRemoteDbLib.get_bulk(__c, keys, atomic) ) );
	}


	/**
		Store records at once in the binary protocol.
		i.e. : setBulk faster
		Note : records set with setBulkBinary must be retrieved with getBulkBinary
	**/
	public function setBulkBinary( recs : Array<BulkRecord> ) : Int {
		var a : Array<Array<Dynamic>> = new Array();
		for( i in 0...recs.length ) {
			var b : BulkRecord = recs[i];
			a.push( [b.key + "",b.value + "",b.expirationTime, if( b.dbidx<0 ) 0 else b.dbidx ] );
		}
		return KTRemoteDbLib.set_bulk_binary( __c, a );
	}


	/**
		Get multiple records at once in the binary protocol.
		i.e. : getBulk faster
	**/
	public function getBulkBinary( keys : Array<String>, previousData : List<BulkRecord> = null, currentTarget : String = null) : List<BulkRecord> {
		var expected = keys.length;
		var base = KTRemoteDbLib.get_bulk_binary(__c, keys, getDbIdx(currentTarget));
		if( base == null ) return null;
		var a = buildArrayArray( base );		
		var b : List<BulkRecord> = if( previousData == null ) new List() else previousData;
		for( i in 0...a.length) {
			var data = a[i];
			var br : BulkRecord = { key: data[0], value:data[1], expirationTime : Std.parseInt(data[2]), dbidx : Std.parseInt( data[3] ) };
			b.push(br);
		}
		var alreadyFound = if( previousData == null ) 0 else previousData.length -1;
		if( expected > b.length - alreadyFound ) {
			// when calling getBulkBinary ( KT 0.9.42 ) the results are retrieved until an error is found (no record found)
			// all the remaining records are then ignored.
			// This behaviour may change in the following versions, however this little patch ensure we'll get the whole
			// data
			// The data will be retrieved in a recursive way
			var found = new Hash();
			for( br in b ) {
				found.set( br.key, "" );
			}
			var otherKeys = new Array();
			for( k in keys ) {
				if( !found.exists( k ) ) {
					otherKeys.push( k );
				}
			}
			return getBulkBinary( otherKeys, b );
		}
		return b;
	}

	/**
		Get multiple records at once in the binary protocol.
		i.e. : getBulk faster
		light method : you cannot get expiration time for each record
	**/
	public function getBulkBinaryLight( keys : Array<String> ) : Hash<String> {
		var base = KTRemoteDbLib.get_bulk_binary(__c, keys, getDbIdx());
		if( base == null ) return null;
		var a = buildArrayArray( base );
		var h = new Hash();
		for( i in 0...a.length ){
			var data = a[i];
			h.set( data[0], data[1] );
		}
		return h;
	}

	/**
		Remove multiple records at once using binary protocl
	**/
	public function removeBulkBinary( keys : Array<String> ) : Int {
		return KTRemoteDbLib.remove_bulk_binary( __c, keys );
	}

	var cursors : List<Cursor>;

	public function cursor() : Cursor {
		var cur = new Cursor(this);
		cur.setPointer( KTRemoteDbLib.cursor( __c ) );
		if( cursors == null ) cursors = new List();
		cursors.add( cur );
		return cur;
	}

/*-------------------------------------------------------------------------------*/


	private function HS2AAS( infos : Hash<String> ) : Array<Array<String>> {
		var a = new Array();
		for( k in infos.keys() ) {
			a.push( [k,infos.get(k) ] );
		}
		return a;
	}

	private function AAS2HS( infos : Array<Array<String>> ) : Hash<String> {
		var h = new Hash();
		for( i in 0...infos.length ) {
			var info = infos[i];
			h.set( info[0], info[1] );
		}
		return h;
	}

	public function buildArray(base) : Array<String> {
		if( base == null ) return null;

		untyped {
			var i = 0;
			var l = __dollar__asize(base);
			while( i < l ) {
				base[i] = new String(base[i]);
				i += 1;
			}
			base = Array.new1(cast base,l);
		}
		return base;
	}

	private function buildArrayArray( base ) : Array<Array<String>> {
		if( base == null ) return null;

		untyped {
			var i = 0;
			var l = __dollar__asize(base);
			while( i < l ) {
				var j = 0;
				var k = __dollar__asize(base[i]);
				while( j < k ) {
					base[i][j] = new String(base[i][j]);
					j += 1;
				}
				base[i] = Array.new1(cast base[i],k);
				i += 1;
			}
			base = Array.new1(cast base,l);
		}
		return base;
	}
}

private class KTRemoteDbLib {
	static function load(fun,args) : Dynamic {
		return neko.Lib.load(lib,fun,args);
	}

	static var lib = "kyototycoon";
	public static var open = load("kt_open",3);
	public static var close = load("kt_close",1);
	public static var clear = load("kt_clear",1);	
	public static var synchronize = load("kt_synchronize",2);	
	public static var count = load("kt_count",1);	
	public static var size = load("kt_size",1);	
	public static var set = load("kt_set",4);	
	public static var add = load("kt_add",4);
	public static var replace = load("kt_replace",4);
	public static var append = load("kt_replace",4);
	public static var increment = load("kt_increment",2);
	public static var increment_double = load("kt_increment_double",2);
	public static var cas = load("kt_cas",5);
	public static var remove = load("kt_remove",2);
	public static var get = load("kt_get",2);
	public static var vacuum = load("kt_vacuum",2);
	public static var match_prefix = neko.Lib.loadLazy(lib, "kt_match_prefix",3);
	public static var match_regex = neko.Lib.loadLazy(lib, "kt_match_regex",3);
	public static var set_target = load("kt_set_target",2);
	public static var expression = load("kt_expression",1);
	public static var status= neko.Lib.loadLazy(lib, "kt_status",1);
	public static var set_bulk = load("kt_set_bulk", 4 );
	public static var remove_bulk = load("kt_remove_bulk", 3 );
	public static var get_bulk = neko.Lib.loadLazy(lib, "kt_get_bulk",3);
	public static var remove_bulk_binary = load("kt_remove_bulk_binary", 2 );
	public static var set_bulk_binary = load("kt_set_bulk_binary", 2 );
	public static var get_bulk_binary = neko.Lib.loadLazy(lib, "kt_get_bulk_binary",3);
	
	public static var cursor = load("kt_cursor",1);
	public static var delete_cursor = load("kt_delete_cursor",1);
	public static var cursor_jump = load("kt_cursor_jump",1);
	public static var cursor_jump_tokey = load("kt_cursor_jump_tokey",2);
	public static var cursor_jump_back = load("kt_cursor_jump_back",1);
	public static var cursor_step = load("kt_cursor_step",1);
	public static var cursor_step_back = load("kt_cursor_step_back",1);
	public static var cursor_remove = load("kt_cursor_remove",1);
	public static var cursor_get_key = load("kt_cursor_get_key",2);
	public static var cursor_get_value = load("kt_cursor_get_value",2);
	public static var cursor_set_value = load("kt_cursor_set_value",4);
	public static var cursor_get = neko.Lib.load(lib, "kt_cursor_get",2);
}
#end
